#include "Ship.h"
#include "SplineHelper.h"
#include "s3e.h"
#include "Iw2D.h"
#include "il_assert.h"
#include <math.h>
#include "ScoreSystem.h"
#include "AudioSystem.h"

extern AudioSystem* SoundSystem;


#define NUM_LINES_PER_SEGMENT 5
#define SCRHEIGHT 320
#define SCRWIDTH 480

#define MAX_NUM_NODES 100
#define CRASH_FLICKER_SPEED 0.12f

#define DOCKING_TIME 3.0f

Ship::Ship() 
{ }

void Ship::Load( ScoreSystem* score_system )
{
	m_flicker_progress = 0.0f;
	m_state = ShipState::UNUSED;
	last_add_tmp = false;
	m_active = false;
	m_yellow_boat.Load( "ship0_yellow" );
	m_red_boat.Load( "ship0_red" );
	m_arrow_left.Load( "arrow_left" );
	m_arrow_right.Load( "arrow_right" );
	m_unload_progress_bg.Load( "unloading_progress_bg" );
	m_unload_progress.Load( "unloading_progress" );
	
	// setup one boat as active sprite
	m_sprite = &m_yellow_boat;
	
	m_pos = CIwFVec2(0, 150);
	
	// store a reference to the score system
	m_score_system = score_system;
	
	m_obb.SetUpCentered( m_pos, CIwFVec2( m_sprite->GetWidth() - 15, m_sprite->GetHeight() - 2), CIwFVec2(0,1) );
	
	// setup the touch area correctly
	m_touch_area.CalcSizeForSprite( *m_sprite );
	m_last_line_node_time = 0;
	
	// setup the ring buffers
	m_nodes_ring.Create( MAX_NUM_NODES );
	m_lines_ring.Create( MAX_NUM_NODES * NUM_LINES_PER_SEGMENT );
}

void Ship::Start( DockType::Enum target_dock, CIwFVec2 p1, CIwFVec2 p2 )
{
	IL_ASSERT( IsActive() == false, "tried to start an already active ship" );

	// clear the path
	m_nodes_ring.Clear();

	m_nodes_ring.Push( p1 - ((p2 - p1) * 0.5f) );
	m_nodes_ring.Push( p1 );
	m_nodes_ring.Push( p2 );
	m_nodes_ring.Push( p2 + ((p2 - p1) * 0.5f) );
	
	m_pos = m_nodes_ring.Peek( 1 );
	m_arc_distance = 0;
	
	PrecalcArcLengthLookup();
	
	m_state = ShipState::ACTIVE_IN;
	
	// set the target dock
	m_target_dock = target_dock;
	
	// and change the sprite to the correct one 
	// for the current type
	if( target_dock == DockType::YELLOW ) {
		m_sprite = &m_yellow_boat;
	} else if( target_dock == DockType::RED ) {
		m_sprite = &m_red_boat;
	}
	
	// save the position for the arrow
	if( m_pos.x > SCRWIDTH ) {
		m_arrow_pos = CIwSVec2( SCRWIDTH - 40, (int) m_nodes_ring.Peek( 2 ).y );
	}
	if( m_pos.x < 0 ) {
		m_arrow_pos = CIwSVec2( 0 + 20, (int) m_nodes_ring.Peek( 2 ).y );
	}
}

CIwFVec2 Ship::GetCurrentDir() const
{
	return m_nodes_ring.Peek(2) - m_nodes_ring.Peek(1);
}

void Ship::Unload()
{
	m_yellow_boat.Unload();
	m_red_boat.Unload();
	m_arrow_left.Unload();
	m_arrow_right.Unload();
	m_unload_progress_bg.Unload();
	m_unload_progress.Unload();
	
	m_nodes_ring.Destroy();
	m_lines_ring.Destroy();
}

void Ship::InvertMovement( const CIwFVec2& dir, const CIwFVec2& normal )
{
	// calculate the reflection vector
	CIwFVec2 reflect = dir - 2 * ( dir.Dot(normal) ) * normal;
	reflect.Normalise();
	
	reflect = reflect * 20.0f;

	m_nodes_ring.TrimAfter(0);
	
	m_nodes_ring.Push( m_pos );
	
	m_nodes_ring.Push( m_pos + reflect );
	m_nodes_ring.Push( m_pos + reflect * 2.0f );
	
	PrecalcArcLengthLookup();
	
	m_lines_ring.Clear();
	
	m_arc_distance = 0;
}

bool Ship::ReachedMaxNodes() const
{
	return m_nodes_ring.GetCount() == m_nodes_ring.GetMaxSize();
}

void Ship::Update( float dt, bool path_editing_active )
{
	if( path_editing_active == false ) m_active = false;

	if( m_state == ShipState::ACTIVE_IN || m_state == ShipState::ACTIVE_OUT ) {
	
		m_last_line_node_time += dt;
		
		if( m_last_line_node_time >= 0.135f ) {
			m_last_line_node_time = 0.0f;
			
			if( m_lines_ring.GetCount() > 0 ) {
				// m_lines_ring.Pop();
			}
		}
	
		float velocity = 30.0f;
		m_arc_distance += velocity * dt;
		
		float nearest = 0xFFFFFF;
		int nearest_id = 0;
		
		if( m_state == ShipState::ACTIVE_IN ) {
			// left border
			if( m_obb.CheckLeftScreenBorderIntersection() ) {
				// check if the next direction point is towards the center of the screen
				CIwFVec2 dir = m_nodes_ring.Peek( 2 ) - m_nodes_ring.Peek( 1 );
				
				if( dir.x < 0 ) {
					InvertMovement( dir, CIwFVec2(1, 0) );
				}
			}
			
			// right border
			if( m_obb.CheckRightScreenBorderIntersection() ) {
				// check if the next direction point is towards the center of the screen
				CIwFVec2 dir = m_nodes_ring.Peek( 2 ) - m_nodes_ring.Peek( 1 );
				
				if( dir.x > 0 ) {
					InvertMovement( dir, CIwFVec2(-1, 0) );
				}
			}
			
			// top border
			if( m_obb.CheckTopScreenBorderIntersection() ) {
				// check if the next direction point is towards the center of the screen
				CIwFVec2 dir = m_nodes_ring.Peek( 2 ) - m_nodes_ring.Peek( 1 );
				
				if( dir.y < 0 ) {
					InvertMovement( dir, CIwFVec2(0, 1) );
				}
			}
			
			// bottom border
			if( m_obb.CheckBottomScreenBorderIntersection() ) {
				// check if the next direction point is towards the center of the screen
				CIwFVec2 dir = m_nodes_ring.Peek( 2 ) - m_nodes_ring.Peek( 1 );
				
				if( dir.y > 0 ) {
					InvertMovement( dir, CIwFVec2(0, -1) );
				}
			}
		}
		
		// if the player clicked on this ship to start drawing a path
		// we have to reset the information we have about the current
		// path
		if( path_editing_active == true ) {
		
			if( m_active == false ) {
			
				CIwFVec2 new_pos( (float)s3ePointerGetTouchX( 0 ), (float)s3ePointerGetTouchY( 0 ) );
			
				if( fabs( 
					(new_pos - m_pos).GetLength()) > 20.0f ) {
					
					// 0 is the node we were before, defines the tangent at 1
					// 1 is the current origin
					// 2 is the target we are moving to
					// 3 is the point fter the target, defines the tangent 2
					// => we have to keep 0/1 and remove all nodes after node 1
					m_nodes_ring.TrimAfter( 0 );
			
					m_nodes_ring.Push( m_pos );
					m_nodes_ring.Push( new_pos );
					
					m_nodes_ring.Push( new_pos );
					
					PrecalcArcLengthLookup();
					m_arc_distance = 0;
					
					m_active = true;
					last_add_tmp = true;
					
					m_lines_ring.Clear();
				}
			} else {
			
				CIwFVec2 new_pos( (float)s3ePointerGetTouchX( 0 ), (float)s3ePointerGetTouchY( 0 ) );
			
				if( fabs( 
					(new_pos - m_nodes_ring.Peek( m_nodes_ring.GetCount() - 1 )).GetLength()) > 20.0f ) {
					
					if( last_add_tmp == true ) {
						m_nodes_ring.TrimAfter( m_nodes_ring.GetCount() - 2 );
					}
					
					if( m_nodes_ring.GetCount() < m_nodes_ring.GetMaxSize() ) {
						m_nodes_ring.Push( new_pos );
						last_add_tmp = false;
						
						
						// add new lines for the next nodes
						for( int i = 0; i < NUM_LINES_PER_SEGMENT; ++i ) {
							m_lines_ring.Push( 
								CatmullRomSpline( m_nodes_ring.Peek(m_nodes_ring.GetCount() -4), m_nodes_ring.Peek(m_nodes_ring.GetCount() -3), m_nodes_ring.Peek( m_nodes_ring.GetCount() -2 ), m_nodes_ring.Peek( m_nodes_ring.GetCount() -1 ), (i / (float) NUM_LINES_PER_SEGMENT)) );
						}
					}
				}
			}
		}
		
		if( m_arc_length_lookup[NUM_SAMPLES+1].arc_length <= m_arc_distance ) {
			// reached the end
			m_arc_distance -= m_arc_length_lookup[NUM_SAMPLES+1].arc_length;
			m_nodes_ring.Pop();
			
			// check if we have any new nodes available
			if( m_nodes_ring.GetCount() < 4 ) {
				// add a new node in the direction the player is currently traveling in
				// to make sure the ship simply travels forward in a straight line
				CIwFVec2 orientation = (m_nodes_ring.Peek( 2 ) - m_nodes_ring.Peek( 1 ));	
				
				// make sure orientation is never zero, if it use the last one
				if( orientation.GetLengthSquared() == 0 ) {
					orientation = m_last_orientation;
				}
				
				m_nodes_ring.Push( m_nodes_ring.Peek( 2 ) + orientation );
			}
			
			for( int i = 0; i < NUM_LINES_PER_SEGMENT; ++i ) {
				if( m_lines_ring.GetCount() > 0 ) {
					m_lines_ring.Pop();
				}
			}
			
			PrecalcArcLengthLookup();
		}
		
		// get the nearest arc_length from the table
		for( int i = 1; i <= NUM_SAMPLES; ++i ) {
			if( fabs(m_arc_distance - m_arc_length_lookup[i].arc_length) < nearest ) {
				nearest_id = i;
				nearest = (float) fabs(m_arc_distance - m_arc_length_lookup[i].arc_length);
			}
		}
		
		// get the +/- 1 t from the table 
		// and do a linear interpoliate
		float t0 = m_arc_length_lookup[nearest_id-1].t;
		float t1 = m_arc_length_lookup[nearest_id+1].t;
		
		float a0 = m_arc_length_lookup[nearest_id-1].arc_length;
		float a1 = m_arc_length_lookup[nearest_id+1].arc_length;
		
		float a = m_arc_distance;
		
		m_t = t0 + ((a - a0) / (a1 - a0)) * (t1 - t0);
		
		if( m_t < 0 ) {
			m_t = 0.0f;
		}
		
		if( m_t > 1.0f ) {
			m_t = 1.0f;
		}
		
		m_last_pos = m_pos;
		m_pos = CatmullRomSpline( m_nodes_ring.Peek(0), m_nodes_ring.Peek(1), m_nodes_ring.Peek(2), m_nodes_ring.Peek(3), m_t);
		
		CIwFVec2 top_left_pos( 
			m_pos.x - m_sprite->GetWidth() / 2,
			m_pos.y - m_sprite->GetHeight() / 2);
		
		// update the OBB	
		CIwFVec2 orientation = (m_pos - m_last_pos);
		
		if( orientation.GetLengthSquared() == 0 ) {
			orientation = m_last_orientation;
		} else {
			m_last_orientation = orientation;
		}
		
		m_last_orientation = m_orientation;
		m_orientation = orientation;
		
		m_obb.UpdateTransform( m_pos, orientation );
		
		// also update the touch area position
		m_touch_area.SetPos( top_left_pos );
	}
	else if( m_state == ShipState::DOCKING ) {
		
		m_docking_time -= dt;
		
		if( m_docking_time <= 0.0f ) {
			m_state = ShipState::ACTIVE_OUT;
			m_score_system->ShipUnloadingFinished( *this );
			
			// the ship is unloaded, we have to clean up the waypoints we have saved
			m_nodes_ring.Clear();
			
			// first first point to define the tangent
			// this is the point we were before
			// we just use the opposie direction of the docking distance
			m_nodes_ring.Push( m_pos - (m_dock_orientation * 20.0f ) );
			m_nodes_ring.Push( m_pos );
			
			// the position we will move to, just along the docking orientation
			m_nodes_ring.Push( m_pos + (m_dock_orientation * 20.0f ) );
			// and one extra node for the tangent at the end
			m_nodes_ring.Push( m_pos + (m_dock_orientation * 20.0f * 2.0f ) );
			
			PrecalcArcLengthLookup();
			m_arc_distance = 0;
			
			return;
		}
		
		CIwFVec2 orientation = m_orientation;
		
		// no we need to rotate?
		float orientation_dot = m_dock_orientation.Dot(orientation);
		if( orientation_dot >= 0.999f ) {
			// set the ship to the final orientation
			orientation.x = m_dock_orientation.x;
			orientation.y = m_dock_orientation.y;
		} else {
			// now calcuate the 2d pseudo cross product to give us the direction we have to roate to
			CIwFVec3 f1 = CIwFVec3( m_dock_orientation.x, m_dock_orientation.y, 0 );
			CIwFVec3 f2 = CIwFVec3( orientation.x, orientation.y, 0 );
			
			float rotation = atan2f(orientation.x, orientation.y);
			
			float rotation_speed = 1.2f;
			
			if( f2.Cross(f1).z > 0 ) {
				// roate clockwise
				rotation -= rotation_speed * dt;
			} else {
				// roate counter clockwise
				rotation += rotation_speed * dt;
			}
			
			orientation = CIwFVec2( 
				(float) sin( rotation ),
				(float) cos( rotation ) );
		}
		
		// store the new orientation
		m_last_orientation = m_orientation;
		m_orientation = orientation;
		
		// ---------------------------------
		m_pos = m_pos + (m_dock_pos - m_pos) * 0.10f;
		
		m_obb.UpdateTransform( m_pos, orientation );
	}
}

void Ship::Draw()
{
	if( m_state == ShipState::UNUSED ) return;

	if( m_target_dock == DockType::RED ) {
		Iw2DSetColour( 0xFF0000C8 );
	} else {
		Iw2DSetColour( 0xFF00E1FF );
	}
	
	for( int i = 0; i < ((int) m_lines_ring.GetCount()) - 1; ++i ) {
		Iw2DDrawLine(
			CIwSVec2( 
				(int) m_lines_ring.Peek(i).x,
				(int) m_lines_ring.Peek(i).y ),
			CIwSVec2(
				(int) m_lines_ring.Peek(i + 1).x,
				(int) m_lines_ring.Peek(i + 1).y ) );
	}
	
	// and one extra line for the last node
	if( m_lines_ring.GetCount() > 0 ) {
		Iw2DDrawLine(
				CIwSVec2( 
					(int) m_lines_ring.Peek( m_lines_ring.GetCount() - 1).x,
					(int) m_lines_ring.Peek( m_lines_ring.GetCount() - 1).y ),
				CIwSVec2(
					(int) m_nodes_ring.Peek( m_nodes_ring.GetCount() - 1).x,
					(int) m_nodes_ring.Peek( m_nodes_ring.GetCount() - 1).y ) );
	}
	Iw2DSetColour( 0xFFFFFFFF );

	CIwFVec2 orientation = m_orientation;
	
	if( orientation.GetLengthSquared() == 0 ) {
		orientation = m_last_orientation;
	} else {
		m_last_orientation = orientation;
	}

	float alpha = 255;
	
	if( m_state == ShipState::CRASHED ) {
	
		if( m_state == ShipState::CRASHED ) {
			m_flicker_progress += CRASH_FLICKER_SPEED;
		}
	
		alpha = 128 + cosf( m_flicker_progress ) * 128;
	}

	uint color = 0x00FFFFFF;
	
	// need to merge the alpha into it
	color = color | ( (uint) (alpha) << 24 );
	
	Iw2DSetColour( color );
	m_sprite->DrawCenteredRotated( m_pos, orientation );
	Iw2DSetColour( 0xFFFFFFFF );
	
	if( m_state == ShipState::ACTIVE_IN ) {
		if( m_pos.x > SCRWIDTH ) {
			m_arrow_right.Draw( m_arrow_pos );
		}
		if( m_pos.x < 0 ) {
			m_arrow_left.Draw( m_arrow_pos );
		}
	}
	
	if( m_state == ShipState::DOCKING ) {
		CIwSVec2 unload_pos( (int)m_pos.x, (int)m_pos.y - 10 );
		m_unload_progress_bg.Draw( unload_pos );
		
		m_unload_progress.DrawStripCentered( unload_pos, (1 - m_docking_time / DOCKING_TIME) );
	}
}

bool Ship::TouchInArea( uint touch )
{
	return m_touch_area.IsWithin( 0 );
}

void Ship::PrecalcArcLengthLookup(  )
{
	float t = 0.0f;
	float arc_length = 0.0f;
	
	CIwFVec2 p0 = m_nodes_ring.Peek(0);
	CIwFVec2 p1 = m_nodes_ring.Peek(1);
	CIwFVec2 p2 = m_nodes_ring.Peek(2);
	CIwFVec2 p3 = m_nodes_ring.Peek(3);
	
	CIwFVec2 last_pos = CatmullRomSpline( p0, p1, p2, p3, t);
	
	m_arc_length_lookup[0].arc_length = 0.0f;
	m_arc_length_lookup[0].t = 0.0f;
	
	for( int i = 1; i < NUM_SAMPLES + 2; ++i ) {
	
		t += (1.0f / (NUM_SAMPLES+1));
		CIwFVec2 new_pos = CatmullRomSpline( p0, p1, p2, p3, t);
		float length = (new_pos - last_pos).GetLength();
		last_pos = new_pos;
		
		arc_length += length;
		
		m_arc_length_lookup[i].arc_length = arc_length;
		m_arc_length_lookup[i].t = t;
	}
}

const OBB& Ship::GetOBB() const
{
	return m_obb;
}

bool Ship::WasAtDock() const
{
	if( m_state == ShipState::ACTIVE_IN ) {
		return false;
	}
	
	return true;
}

void Ship::Crashed()
{
	switch(rand()%3)
	{
	case 0:
		SoundSystem->Play("crashwood_a");
		break;
	case 1:
		SoundSystem->Play("crashwood_b");
		break;
	case 2:
		SoundSystem->Play("crashwood_c");
		break;
	}
	m_state = ShipState::CRASHED;
	m_flicker_progress = 0;
}

bool Ship::IsDocking() const
{
	if( m_state == ShipState::DOCKING ) {
		return true;
	}
	
	return false;
}

void Ship::StartDocking( const Dock& dock )
{
	(void) dock;
	
	// get the position we need to dock to
	// the docking happens in a small area
	// we will simply linearly inpoliate into the 
	// docking position
	m_state = ShipState::DOCKING;
	m_docking_time = 3.0f;
	m_dock_pos = dock.GetDockingPos();
	
	m_dock_orientation = dock.GetDockingOrientation();
	
	m_lines_ring.Clear();
	
	// check which dircetion we should dock to
	if( m_orientation.Dot( m_dock_orientation ) <= 0 ) {
		// dock to the inverse
		m_dock_orientation = -m_dock_orientation;
	}
}

bool Ship::IsActive() const
{
	if( m_state == ShipState::UNUSED ) {
		return false;
	} else {
		return true;
	}
}

CIwFVec2 Ship::GetPos() const
{
	return m_pos;
}

bool Ship::OutsideScreen() const
{
	// outside left border
	if( m_pos.x <= -m_sprite->GetWidth() ) {
		return true;
	}
	
	// outside right border
	if( m_pos.x >= SCRWIDTH + m_sprite->GetWidth() ) {
		return true;
	}
	
	// outside top border
	if( m_pos.y <= -m_sprite->GetHeight() ) {
		return true;
	}
	
	// outside the bottom border
	if( m_pos.y >= SCRHEIGHT + m_sprite->GetHeight() ) {
		return true;
	}
	
	return false;
}

void Ship::Disable()
{
	m_state = ShipState::UNUSED;
}

DockType::Enum Ship::GetTargetDock() const
{
	return m_target_dock;
}

bool Ship::HasCrashed() const
{
	return m_state == ShipState::CRASHED;
}